// Copyright (c) 2007, Brian Duff

// See LICENSE for full license details.

package org.dubh.jdant;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;


/**
 * Scans a JDeveloper installation directory for libraries.
 *
 * @author brian.duff@dubh.org
 */
final class LibraryScanner
{
  private static final String XMLNS_LIBRARIES = 
    "http://xmlns.oracle.com/jdeveloper/1013/jdev-libraries";
  
  private static final String XMLNS_JSR198 =
    "http://jcp.org/jsr/198/extension-manifest";
  
  private static final String LIBRARY = "library";
  private static final String CLASSPATH = "classpath";
  private static final String EXTENSION = "extension";
  
  private final File _oracleHome;
  private static final String[] EXTENSION_DIRS = 
  {
    "jdev" + File.separator + "extensions",
    "ide" + File.separator + "extensions",
    "sqldeveloper" + File.separator + "extensions"
  };
  
  private Collection<String> exportedIds = null;
  
  
  LibraryScanner( File oracleHome )
  {
    _oracleHome = oracleHome;
  }

  void setExportedIds( Collection<String> exportedIds )
  {
    this.exportedIds = exportedIds;
  }

  private boolean isExported( String id )
  {
    if ( exportedIds == null ) return false;
    return exportedIds.contains( id );
  }

  /**
   * Find all system libraries defined in the oracle home associated with this
   * object.
   * 
   * @return a list of libraries.
   */
  public Map<String,Library> findLibraries()
  {
    Map<String,Library> libraries = new HashMap<String,Library>();
    Collection<File> extensionDirs = findExtensionDirectories();
    
    for ( File extensionDir : extensionDirs )
    {
      findLibraries( extensionDir, libraries );
    }
    
    return Collections.unmodifiableMap( libraries );
  }
  
  private void findLibraries( File extensionDir, Map<String,Library> libraries )
  {
    File[] extensionJars = extensionDir.listFiles( new FilenameFilter()
    {      
      public boolean accept(File dir, String name)
      {
        return name != null && name.toLowerCase().endsWith( ".jar" );
      }
    });
    
    for ( File extensionJar : extensionJars )
    {
      findLibrariesInExtension( extensionJar, libraries );
    }
  }
  
  private void findLibrariesInExtension( File extensionJar, 
    Map<String,Library> libraries )
  {
    URL manifestUrl = newJarEntryUrl( extensionJar, "META-INF/extension.xml" );
    
    InputStream stream = null;
    try
    {
      stream = manifestUrl.openStream();
      ManifestLibraryHandler handler = new ManifestLibraryHandler( 
        extensionJar.getParentFile(), libraries );
      Util.newSAXParser().parse( stream, handler );
    }
    catch ( ProcessingDoneException e )
    {
      return;
    }
    catch ( SAXException e )
    {
      System.err.println( "Failed to read " + manifestUrl + ": " + e );
    }
    catch ( IOException e )
    {
      System.err.println( "Failed to read " + manifestUrl );
    }
    finally
    {
      Util.close( stream );
    }
  }
  
  private URL newJarEntryUrl( File jarFile, String entryPath )
  {
    try
    {
      URL fileUrl = jarFile.toURI().toURL();
      return new URL( "jar:" + fileUrl.toExternalForm() + "!/" + entryPath );
    }
    catch ( MalformedURLException e )
    {
      throw new IllegalArgumentException( String.valueOf( jarFile ) );
    }
  }
  
  private Collection<File> findExtensionDirectories()
  {
    // Would be better to use the information in product.boot to auto-
    // discover these...
    Collection<File> dirs = new ArrayList<File>();
    for ( String s : EXTENSION_DIRS )
    {
      File dir = Util.normalize( new File( _oracleHome.getPath() + File.separator + s ) );
      if ( dir.isDirectory() ) dirs.add( dir );
    }
    return dirs;
  }
  
  private class ManifestLibraryHandler extends DefaultHandler
  {
    private final File _baseDir;
    private final Map<String,Library> _libraries;
    private String _currentLibraryName;
    private List<File> _currentLibraryClasspath;
    private StringBuilder _text;
    private String _extensionId;

    ManifestLibraryHandler( File baseDir, Map<String,Library> libraries )
    {
      _baseDir = baseDir;
      _libraries = libraries;
    }

    @Override
    public void startElement(String uri, String localName, String qName,
                             Attributes attributes)
      throws SAXException
    {
      if ( isExtension( uri, localName ) )
      {
        startExtension( attributes );
        return;
      }
      
      if ( isLibrary( uri, localName ) )
      {
        startLibrary( attributes );
        return;
      }
      
      if ( isClasspath( uri, localName ) )
      {
        startClasspath();
      }
    }

    @Override
    public void endElement(String uri, String localName, String qName)
      throws SAXException
    {
      if ( isLibraries( uri, localName ) )
      {
        throw new ProcessingDoneException();
      }
      
      if ( isLibrary( uri, localName ) )
      {
        endLibrary();
        return;
      }
      
      if ( isClasspath( uri, localName ) )
      {
        endClasspath();
      }
    }

    @Override
    public void characters(char[] c, int start, int length)
      throws SAXException
    {
      if ( _text != null ) _text.append( c, start, length );
    }
    
    private void startExtension( Attributes attributes )
    {
      _extensionId = attributes.getValue( "id" );
    }
    
    private void startClasspath()
    {
      _text = new StringBuilder();
    }
    
    private void endClasspath()
    {
      String text = _text.toString().trim();
      
      text = text.replace( "${ide.extension.install.home}", _extensionId );
      
      File path = Util.normalize(new File( _baseDir.toString() + File.separator + text ));
      _currentLibraryClasspath.add( path );
      
      _text = null;
    }
    
    private void startLibrary( Attributes attributes )
    {
      assert _currentLibraryName == null;
      _currentLibraryName = attributes.getValue( "name" );
      _currentLibraryClasspath = new ArrayList<File>();
    }
    
    private void endLibrary()
    {
      Library library = 
        new Library.Builder( _currentLibraryName, _currentLibraryClasspath )
          .exported( isExported( _currentLibraryName ) )
          .build();
      _libraries.put( _currentLibraryName, library );
      
      _currentLibraryClasspath = null;
      _currentLibraryName = null;
    }
    
    private boolean isLibraries( String uri, String local )
    {
      return isElement( XMLNS_LIBRARIES, "libraries", uri, local );
    }
    
    private boolean isClasspath( String uri, String local )
    {
      return isElement( XMLNS_LIBRARIES, CLASSPATH, uri, local );
    }
    
    private boolean isLibrary( String uri, String local )
    {
      return isElement( XMLNS_LIBRARIES, LIBRARY, uri, local );
    }
    
    private boolean isExtension( String uri, String local )
    {
      return isElement( XMLNS_JSR198, EXTENSION, uri, local );
    }
    
    private boolean isElement( String expectedUri, String expectedLocal, 
                               String actualUri, String actualLocal )
    {
      return expectedLocal.equals( actualLocal ) && 
             expectedUri.equals( actualUri );
    }
  }

  /**
   * Runtime exception used to shortcircuit parsing the extension manifest
   * as soon as we've found library information.
   */
  private static class ProcessingDoneException extends RuntimeException {
    private static final long serialVersionUID = 4746508767818375805L;
  }
}
